/*
 * from http://www.coderanch.com/t/346509/GUI/java/JTree-drag-drop-tree-Java
 */
package avl.sv.shared;

import avl.sv.shared.Annotations.Annotation;
import avl.sv.shared.Annotations.AnnotationSet;
import avl.sv.shared.Annotations.ROI;
import java.awt.datatransfer.*;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import javax.swing.tree.*;
import org.jdesktop.swingx.JXTreeTable;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
import org.jdesktop.swingx.treetable.MutableTreeTableNode;
import org.jdesktop.swingx.treetable.TreeTableNode;

public class TreeTransferHandler extends TransferHandler {

    DataFlavor nodesFlavor;
    DataFlavor[] flavors = new DataFlavor[1];
    DefaultMutableTreeTableNode[] nodesToRemove;
    JXTreeTableROIs jTreeTableSrc, jTreeTableDest;
    
    static TreeTransferHandler instance = null;
    public static TreeTransferHandler getInstance(){
        if (instance == null) {
            instance = new TreeTransferHandler();
        }
        return instance;
    }
        
    public TreeTransferHandler() {
        try {
            String mimeType = DataFlavor.javaJVMLocalObjectMimeType
                    + ";class=\""
                    + DefaultMutableTreeTableNode[].class.getName()
                    + "\"";
            nodesFlavor = new DataFlavor(mimeType);
            flavors[0] = nodesFlavor;
        } catch (ClassNotFoundException e) {
            System.out.println("ClassNotFound: ");
            e.printStackTrace(System.out);
        }
    }

    @Override
    public boolean canImport(TransferHandler.TransferSupport support) {
        jTreeTableDest = (JXTreeTableROIs) support.getComponent();
        if (jTreeTableDest == null){
            return false;
        }
        
        if (!support.isDrop()) {
            return false;
        }
        support.setShowDropLocation(true);
        if (!support.isDataFlavorSupported(nodesFlavor)) {
            return false;
        }
        // Check for dummy node created in this class
        DefaultMutableTreeTableNode[] transferData;
        try {
            Transferable t = support.getTransferable();
            transferData = (DefaultMutableTreeTableNode[]) t.getTransferData(nodesFlavor);
            if (transferData.length == 0){
                return false;
            }
        } catch (UnsupportedFlavorException | IOException ex) {
            ex.printStackTrace(System.out);
        }
        
        JTable.DropLocation dl = (JTable.DropLocation) support.getDropLocation();
        // false if dest is null 
        if ( dl.getRow() == -1 ){
            return false;
        }
        
        // Do not allow a drop on the drag source selections.    
        if (jTreeTableDest.equals(jTreeTableSrc)){
            int dropRow = dl.getRow();
            for (int selRow:jTreeTableSrc.getSelectedRows()) {
                if (selRow == dropRow) {
                    return false;
                }
            }        
        }
        
        // only allow transfer from within one catagory/group        
        Object[] firstPath = jTreeTableSrc.getSelectionPath().getPath();
        for (TreePath tPath:jTreeTableSrc.getSelectionPaths()){
            Object[] path = tPath.getPath();
            boolean same = path.length == firstPath.length;
            for (int i = 0; (i < path.length-1) && (i < firstPath.length-1); i++){
                same  &= path[i].equals(firstPath[i]);
            }
            if (!same){
                return false;
            }
        }
        
        // Make sure AnnotationSets are the same
        AnnotationSet srcAnnoSet = null;
        for(Object node:jTreeTableSrc.getSelectionPath().getPath()){
            if (node instanceof AnnotationSet){
                srcAnnoSet = (AnnotationSet) node;
            }
        }
        TreePath destNode = jTreeTableDest.getPathForLocation(dl.getDropPoint().x, dl.getDropPoint().y);
        if (destNode == null){
            return false;
        }
        AnnotationSet destAnnoSet = null;
        for (Object node:destNode.getPath()){
            if (node instanceof AnnotationSet){
                destAnnoSet = (AnnotationSet) node;
            }
        }
        if ((srcAnnoSet != null) && (destAnnoSet != null)){
            byte[] srcHash = srcAnnoSet.getHash();
            byte[] destHash = destAnnoSet.getHash();
            if (!ImageID.hashesAreEqual(srcHash, destHash)){
                return false;
            }
        }
        
        // only allow specific transfer modes
        Object srcObj = jTreeTableSrc.getLastSelectedPathComponent();
        Boolean srcObjIsROI =           srcObj instanceof ROI;
        Boolean srcObjIsAnnotation =    srcObj instanceof Annotation;
        
        Object dstObj = destNode.getLastPathComponent();
        Boolean dstObjIsAnnotation = dstObj instanceof Annotation;
        Boolean dstObjIsAnnotationSet = dstObj instanceof AnnotationSet;  
        Boolean dstObjIsClassifierClass = dstObj instanceof ClassifierClass;  
        
        boolean ok = false;
        if (srcObjIsROI && (dstObjIsAnnotation || dstObjIsClassifierClass || dstObjIsAnnotationSet || srcObjIsROI)){
           ok|=true;
        }
        if (srcObjIsAnnotation && (dstObjIsAnnotationSet || dstObjIsClassifierClass )){
           ok|=true;
        }
        if (!ok){
           return false;
        }
        
        // Do not allow MOVE-action drops if a non-leaf node is
        // selected unless all of its children are also selected.
        int action = support.getDropAction();
        if (action == MOVE) {
            return haveCompleteNode(jTreeTableSrc);
        }
        return true;
    }

    private boolean haveCompleteNode(JXTreeTableROIs tree) {
        ArrayList<MutableTreeTableNode> selectedNodes = new ArrayList<>();
        for (TreePath path:tree.getSelectionPaths()){
            selectedNodes.add((MutableTreeTableNode)path.getLastPathComponent());
        }
        if (selectedNodes.isEmpty()){
            return true;
        }
        
        for ( MutableTreeTableNode node:selectedNodes){
            Enumeration<? extends MutableTreeTableNode> children = node.children();
            while (children.hasMoreElements()){
                MutableTreeTableNode child = children.nextElement();
                if (!selectedNodes.contains(child)){
                    return false;
                }
            }
        }
        return true;
    }

    private boolean containsROIsAndAnnotations(TreePath[] paths){
        Boolean containsROIs = false, containsAnnotations = false;
        for (TreePath p:paths){
            Object comp = p.getLastPathComponent();
            if (comp instanceof Annotation){
                containsAnnotations = true;
            }
            if (comp instanceof ROI){
                containsROIs = true;
            }
        }
        if (containsROIs && containsAnnotations){
            return true;
        } else {
            return false;
        }
    }
    
    @Override
    protected Transferable createTransferable(JComponent c) {
        jTreeTableSrc = (JXTreeTableROIs) c;
        TreePath[] paths = jTreeTableSrc.getSelectionPaths();
        if (paths == null) {
            return null;
        }
        
        // Check if contains rois and annotations. Don't want to handle this case
        if (containsROIsAndAnnotations( paths )){
            return new NodesTransferable(new DefaultMutableTreeTableNode[0]);
        }
        
        ArrayList<DefaultMutableTreeTableNode> nodesArrayList = new ArrayList<>();
        for (TreePath path:paths){
            nodesArrayList.add((DefaultMutableTreeTableNode)path.getLastPathComponent());
        }

        DefaultMutableTreeTableNode[] nodes = nodesArrayList.toArray(new DefaultMutableTreeTableNode[nodesArrayList.size()]);
        nodesToRemove = nodes;
        return new NodesTransferable(nodes);
    }

    private DefaultMutableTreeTableNode copy(TreeNode node) {
        return new DefaultMutableTreeTableNode(node);
    }

    @Override
    protected void exportDone(JComponent source, Transferable data, int action) {
        if (jTreeTableSrc == null || jTreeTableDest == null){
            return;
        }
        if (jTreeTableDest.equals(jTreeTableSrc)){
            if ((action & MOVE) == MOVE) {
                JXTreeTable tree = (JXTreeTable) source;
                DefaultTreeTableModel model = (DefaultTreeTableModel) tree.getTreeTableModel();
                // Remove nodes saved in nodesToRemove in createTransferable.
                for (int i = 0; i < nodesToRemove.length; i++) {
                    model.removeNodeFromParent(nodesToRemove[i]);
                }
            }
        }
    }

    @Override
    public int getSourceActions(JComponent c) {
        return COPY_OR_MOVE;
    }

    @Override
    public boolean importData(TransferHandler.TransferSupport support) {
        jTreeTableDest = (JXTreeTableROIs) support.getComponent();
        DefaultTreeTableModel model = (DefaultTreeTableModel) jTreeTableDest.getTreeTableModel();
        if (!canImport(support)) {
            return false;
        }
        // Extract transfer data.
        DefaultMutableTreeTableNode[] nodes = null;
        try {
            Transferable t = support.getTransferable();
            nodes = (DefaultMutableTreeTableNode[]) t.getTransferData(nodesFlavor);
        } catch (UnsupportedFlavorException ufe) {
            System.out.println("UnsupportedFlavor: " + ufe.getMessage());
            ufe.printStackTrace(System.out);
        } catch (java.io.IOException ioe) {
            System.out.println("I/O error: " + ioe.getMessage());
            ioe.printStackTrace(System.out);
        }
        // Get drop location info.
        JTable.DropLocation dl = (JTable.DropLocation) support.getDropLocation();
        TreePath dest = jTreeTableDest.getPathForRow(dl.getRow());
        DefaultMutableTreeTableNode parent = (DefaultMutableTreeTableNode) dest.getLastPathComponent();
                
        // Add data to model.
        for (DefaultMutableTreeTableNode node:nodes) {
            
            // get the root of parent
            TreeTableNode root = (TreeTableNode) jTreeTableDest.getTreeTableModel().getRoot();

            if (root instanceof Solution){
                // For droppint on classifier tree
                AnnotationSet srcAnnoSet;   
                Annotation srcAnno       = null;
                ROI srcROI               = null;
                
                ClassifierClass destClassifierClass = null;
                AnnotationSet   destAnnoSet         = null;
                Annotation      destAnno            = null;               
                
                if (node instanceof Annotation) {
                    srcAnno = (Annotation) node;
                } else if (node instanceof ROI) {
                    srcROI = (ROI) node;
                    srcAnno = (Annotation) srcROI.getParent();                    
                }
                srcAnnoSet = (AnnotationSet) srcAnno.getParent();

                if (parent instanceof ClassifierClass){
                    destClassifierClass = (ClassifierClass)parent;
                } else if (parent instanceof AnnotationSet){
                    destAnnoSet = (AnnotationSet)parent;
                    destClassifierClass = (ClassifierClass)destAnnoSet.getParent();
                } else if (parent instanceof Annotation){
                    destAnno = (Annotation)parent;   
                    destClassifierClass = (ClassifierClass)destAnno.getParent().getParent();
                } else if (parent instanceof ROI){
                    destAnno = (Annotation)parent.getParent();
                    destClassifierClass = (ClassifierClass)destAnno.getParent().getParent();
                }
                                
                if (destAnnoSet == null){
                    destAnnoSet = getAnnotationSetForHash(destClassifierClass, srcAnnoSet.getHash());
                    if (destAnnoSet == null) {
                        destAnnoSet = new AnnotationSet(srcAnnoSet.getSlideReference());
                        model.insertNodeInto((DefaultMutableTreeTableNode)destAnnoSet, (DefaultMutableTreeTableNode)destClassifierClass, destClassifierClass.getChildCount());
                    }
                }            
                
                if (destAnno == null) {
                    destAnno = getDefaultAnno(destAnnoSet);
                    if (destAnno == null) {
                        destAnno = Annotation.createDefault();
                        destAnno.name = "Default";
                        model.insertNodeInto((DefaultMutableTreeTableNode)destAnno, (DefaultMutableTreeTableNode)destAnnoSet, destAnnoSet.getChildCount());
                    }
                } 
                // Configure for drop mode.
                int index;
                if (parent instanceof ROI){
                    index = ((DefaultMutableTreeTableNode)dest.getLastPathComponent()).getIndex((DefaultMutableTreeTableNode)dest.getLastPathComponent());// DropMode.INSERT
                    if (index == -1) {              // DropMode.ON
                        index = parent.getChildCount();
                    }
                } else {
                    index = destAnno.getChildCount();
                }
                model.insertNodeInto((DefaultMutableTreeTableNode) (srcROI.clone()), (DefaultMutableTreeTableNode) destAnno, index);
            } else {
                int index = ((DefaultMutableTreeTableNode)dest.getLastPathComponent()).getIndex((DefaultMutableTreeTableNode)dest.getLastPathComponent());// DropMode.INSERT
                // Configure for drop mode.
                if (index == -1) {              // DropMode.ON
                    index = parent.getChildCount();
                }
                if (node instanceof ROI){
                    ROI roi = (ROI) node;
                    model.insertNodeInto((DefaultMutableTreeTableNode) roi.clone(), parent, index++);
                }
            }
        }
        return true;
    }
    
    private AnnotationSet getAnnotationSetForHash(ClassifierClass cc,  byte hash[]) {
        for (AnnotationSet annoSet : cc.getAnnotationSets()) {
            if (ImageID.hashesAreEqual(hash, annoSet.getHash())){
                return annoSet;
            }
        }
        return null;
    }
    
    private Annotation getDefaultAnno(AnnotationSet annoSet){
        for (Annotation anno:annoSet.getAnnotations()){
            if ("Default".equals(anno.name)){
                return anno;
            }
        }
        return null;
    }
    
    @Override
    public String toString() {
        return getClass().getName();
    }

    public class NodesTransferable implements Transferable {

        DefaultMutableTreeTableNode[] nodes;

        public NodesTransferable(DefaultMutableTreeTableNode[] nodes) {
            this.nodes = nodes;
        }

        @Override
        public Object getTransferData(DataFlavor flavor)
                throws UnsupportedFlavorException {
            if (!isDataFlavorSupported(flavor)) {
                throw new UnsupportedFlavorException(flavor);
            }
            return nodes;
        }

        @Override
        public DataFlavor[] getTransferDataFlavors() {
            return flavors;
        }

        @Override
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return nodesFlavor.equals(flavor);
        }
    }
}
